import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.text.DecimalFormat;
import java.util.ArrayList;

public class GamePanel extends JPanel implements MouseListener, 
                                                       MouseMotionListener,
                                                       KeyListener,
                                                       Runnable {

	private static final long serialVersionUID = 7526472295622776147L;
	private ArrayList <Actor> actors;
	
	private Map gameMap;
	int _x, _y;	// mouse x,y
	
	private ImageManager imageManager;
	private InputManager inputManager;
	
    private static final int PWIDTH = 640;   // size of panel
    private static final int PHEIGHT = 480; 

    private static double MAX_STATS_INTERVAL = 1000000000L;
      // record stats every 1 second (roughly)

    private static final int NO_DELAYS_PER_YIELD = 16;
    /* Number of frames with a delay of 0 ms before the animation thread yields
       to other running threads. */

    private static int MAX_FRAME_SKIPS = 5;   // was 2;
    // no. of frames that can be skipped in any one animation loop
    // i.e the games state is updated but not rendered

    private static int NUM_FPS = 10;
    // number of FPS values stored to get an average


    // used for gathering statistics
    private double statsInterval = 0L;    // in ms
    private double prevStatsTime;   
    private double totalElapsedTime = 0L;
    private double elapsedTime = 0L;
    private double gameStartTime;
    private int timeSpentInGame = 0;    // in seconds

    private double frameCount = 0;
    private double fpsStore[];
    private double statsCount = 0;
    private double averageFPS = 0.0;

    private double framesSkipped = 0L;
    private double totalFramesSkipped = 0L;
    private double upsStore[];
    private double averageUPS = 0.0;
    
    private DecimalFormat df = new DecimalFormat("0.##");  // 2 dp
    //private DecimalFormat timedf = new DecimalFormat("0.####");  // 4 dp
    
    private Thread animator;           // the thread that performs the animation
    private volatile boolean running = false;   // used to stop the animation thread
    private volatile boolean isPaused = false;

    private double period;                // period between drawing in _nanoseconds_

    private ResourceHogs rhTop;

    // used at game termination
    private volatile boolean gameOver = false;
    private int score = 0;
    private Font font;
    private FontMetrics metrics;

    // off screen rendering
    private Graphics dbg; 
    private Image dbImage = null;
    
    public GamePanel(ResourceHogs rh, double period)
    {
        rhTop = rh;
        this.period = period;

        setBackground(Color.white);
        setPreferredSize( new Dimension(PWIDTH, PHEIGHT));

        setFocusable(true);
        requestFocus();    // the JPanel now has focus, so receives key events

        // initialise timing elements
        fpsStore = new double[NUM_FPS];
        upsStore = new double[NUM_FPS];
        for ( int i=0; i < NUM_FPS; i++ )
        {
			fpsStore[i] = 0.0;
			upsStore[i] = 0.0;
        }
        
        //--- Setup game components
        imageManager = new ImageManager();
        inputManager = new InputManager();
        actors = new ArrayList<Actor>();
        
        // setup player 1
        Animation player1Anim = new Animation();
        imageManager.loadImage("img/ship1.png");
        player1Anim.addFrame(imageManager.getImage("img/ship1.png"), 1000);
        Player player1 = new Player("Grod", player1Anim, inputManager);
        inputManager.map(player1.getName(), InputManager.EVENT_UP, KeyEvent.VK_W);
        inputManager.map(player1.getName(), InputManager.EVENT_DOWN, KeyEvent.VK_S);
        inputManager.map(player1.getName(), InputManager.EVENT_LEFT, KeyEvent.VK_A);
        inputManager.map(player1.getName(), InputManager.EVENT_RIGHT, KeyEvent.VK_D);
        actors.add(player1);
        
        // setup player 2
        Animation player2Anim = new Animation();
        imageManager.loadImage("img/ship2.png");
        player2Anim.addFrame(imageManager.getImage("img/ship2.png"), 1000);
        Player player2 = new Player("Sallad Wog", player2Anim, inputManager);
        inputManager.map(player2.getName(), InputManager.EVENT_UP, KeyEvent.VK_UP);
        inputManager.map(player2.getName(), InputManager.EVENT_DOWN, KeyEvent.VK_DOWN);
        inputManager.map(player2.getName(), InputManager.EVENT_LEFT, KeyEvent.VK_LEFT);
        inputManager.map(player2.getName(), InputManager.EVENT_RIGHT, KeyEvent.VK_RIGHT);
        actors.add(player2);
        
        // setup the link between the two players
        Animation linkAnim = new Animation();
        imageManager.loadImage("img/link.png");
        linkAnim.addFrame(imageManager.getImage("img/link.png"), 1000);
        Link link = new Link(linkAnim, player1, player2);
        actors.add(link);
     
        // setup the game map
        gameMap = new Map("Mappy", imageManager.loadImage("img/background.png"));
        
        //--- Add the mouse listeners.
        this.addMouseListener(this); 
        this.addMouseMotionListener(this);
        
        //--- Add the keyboard listener
        this.addKeyListener(this);
    }
    
    public void addNotify()
    {
    	super.addNotify();   // creates the peer
    }
    
    public void startGame() {
    	if (animator == null || !running) {
    		animator = new Thread(this);
    		animator.start();
    	}
    }
    
    public void resumeGame() {
    	isPaused = false;
    } 


    public void pauseGame() {
    	isPaused = true;
    } 


    public void stopGame() {
    	running = false;
    }
    
    public void run() {
    	double beforeTime, afterTime, timeDiff, sleepTime, overSleepTime = 0;
    	int noDelays = 0;
    	double excess = 0L;

    	gameStartTime = System.nanoTime();
    	prevStatsTime = gameStartTime;
    	beforeTime = gameStartTime;

    	running = true;

    	while(running) {
    		gameUpdate(elapsedTime); 
    		gameRender();   // render the game to a buffer
    		paintScreen();  // draw the buffer on-screen

    		afterTime = System.nanoTime();
    		timeDiff = afterTime - beforeTime;
    		elapsedTime = timeDiff;
    		sleepTime = (period - timeDiff) - overSleepTime; 
    		
    		if (sleepTime > 0) {   // some time left in this cycle
    			try {
    				Thread.sleep((long)sleepTime/1000000L);  // convert from ns to ms
    				noDelays = 0;
    			}
    			catch(InterruptedException ex){}
    			overSleepTime = (System.nanoTime() - afterTime) - sleepTime;
    		}
    		else {    // sleepTime <= 0; the frame took doubleer than the period
    			excess -= sleepTime;  // store excess time value
    			overSleepTime = 0L;

    			if (++noDelays >= NO_DELAYS_PER_YIELD) {
    				Thread.yield();   // give another thread a chance to run
    				noDelays = 0;
    			}
    		}

    		beforeTime = System.nanoTime();

    		/* If frame animation is taking too double, update the game state
           without rendering it, to get the updates/sec nearer to
           the required FPS. */
    		int skips = 0;
    		while((excess > period) && (skips < MAX_FRAME_SKIPS)) {
    			excess -= period;
    			gameUpdate(elapsedTime);    // update state but don't render
    			skips++;
    		}
    		framesSkipped += skips;

    		storeStats();
    	}

    	rhTop.stop();	  // set original display mode
    	printStats();
    	System.exit(0);   // so window disappears
    } // end of run()

    
    private void gameUpdate(double elapsedTime) {
    	if (!isPaused && !gameOver) {
    		for (int i=0; i < actors.size(); i++) {
    			Actor a = (Actor)actors.get(i);
    			
    			if (a.markedForDeath) {
    				actors.remove(i);
    			}
    			else {
    				a.update(elapsedTime);
    			}
    		}
    		gameMap.update(elapsedTime);
    	}
    }//end gameUpdate

    private void gameRender() {
    	if (dbImage == null) {
    		dbImage = createImage(PWIDTH, PHEIGHT);
    		if (dbImage == null) {
    			System.out.println("dbImage is null");
    			return;
    		}
    		else
    			dbg = dbImage.getGraphics();
    	}
 	
    	// draw the game elements
    	gameMap.draw(dbg);
		for (int i=0; i < actors.size(); i++) {
			Actor a = (Actor)actors.get(i);
			a.draw(dbg);
		}
    	
		// draw the hud
    	// report frame count & average FPS and UPS at top left
    	dbg.setColor(Color.red);
    	dbg.setFont(font);
    	dbg.drawString("Average FPS/UPS: " + df.format(averageFPS) + ", " +
    			df.format(averageUPS), 20, 25);  // was (10,55)

    	if (gameOver)
    		gameOverMessage(dbg);
    }  // end of gameRender()
    
    private void gameOverMessage(Graphics g)
    // center the game-over message in the panel
    {
    	String msg = "Game Over. Your Score: " + score;
    	int x = (PWIDTH - metrics.stringWidth(msg))/2; 
    	int y = (PHEIGHT - metrics.getHeight())/2;
    	g.setColor(Color.red);
    	g.setFont(font);
    	g.drawString(msg, x, y);
    }  // end of gameOverMessage()


    private void paintScreen()
    // use active rendering to put the buffered image on-screen
    { 
    	Graphics g;
    	try {
    		g = this.getGraphics();
    		if ((g != null) && (dbImage != null))
    			g.drawImage(dbImage, 0, 0, null);
    		Toolkit.getDefaultToolkit().sync();  // sync the display on some systems
    		g.dispose();
    	}
    	catch (Exception e)
    	{ System.out.println("Graphics error: " + e);  }
    } // end of paintScreen()


    private void storeStats()
    /* The statistics:
         - the summed periods for all the iterations in this interval
           (period is the amount of time a single frame iteration should take), 
           the actual elapsed time in this interval, 
           the error between these two numbers;

         - the total frame count, which is the total number of calls to run();

         - the frames skipped in this interval, the total number of frames
           skipped. A frame skip is a game update without a corresponding render;

         - the FPS (frames/sec) and UPS (updates/sec) for this interval, 
           the average FPS & UPS over the last NUM_FPSs intervals.

       The data is collected every MAX_STATS_INTERVAL  (1 sec).
    */
    { 
      frameCount++;
      statsInterval += period;

      if (statsInterval >= MAX_STATS_INTERVAL) {     // record stats every MAX_STATS_INTERVAL
        double timeNow = System.nanoTime();
        timeSpentInGame = (int) ((timeNow - gameStartTime)/1000000000L);  // nanos --> secs

        double realElapsedTime = timeNow - prevStatsTime;   // time since last stats collection
        totalElapsedTime += realElapsedTime;

        //double timingError = ((double)(realElapsedTime - statsInterval) / statsInterval) * 100.0;

        totalFramesSkipped += framesSkipped;

        double actualFPS = 0;     // calculate the latest FPS and UPS
        double actualUPS = 0;
        if (totalElapsedTime > 0) {
          actualFPS = (((double)frameCount / totalElapsedTime) * 1000000000L);
          actualUPS = (((double)(frameCount + totalFramesSkipped) / totalElapsedTime) * 1000000000L);
        }

        // store the latest FPS and UPS
        fpsStore[ (int)statsCount%NUM_FPS ] = actualFPS;
        upsStore[ (int)statsCount%NUM_FPS ] = actualUPS;
        statsCount = statsCount+1;

        double totalFPS = 0.0;     // total the stored FPSs and UPSs
        double totalUPS = 0.0;
        for (int i=0; i < NUM_FPS; i++) {
          totalFPS += fpsStore[i];
          totalUPS += upsStore[i];
        }

        if (statsCount < NUM_FPS) { // obtain the average FPS and UPS
          averageFPS = totalFPS/statsCount;
          averageUPS = totalUPS/statsCount;
        }
        else {
          averageFPS = totalFPS/NUM_FPS;
          averageUPS = totalUPS/NUM_FPS;
        }
  /*
        System.out.println(timedf.format( (double) statsInterval/1000L) + " " + 
                      timedf.format((double) realElapsedTime/1000L) + "s " + 
  			        df.format(timingError) + "% " + 
                      frameCount + "c " +
                      framesSkipped + "/" + totalFramesSkipped + " skip; " +
                      df.format(actualFPS) + " " + df.format(averageFPS) + " afps; " + 
                      df.format(actualUPS) + " " + df.format(averageUPS) + " aups" );
  */
        framesSkipped = 0;
        prevStatsTime = timeNow;
        statsInterval = 0L;   // reset
      }
    }  // end of storeStats()


    private void printStats()
    {
    	System.out.println("Frame Count/Loss: " + frameCount + " / " + totalFramesSkipped);
    	System.out.println("Average FPS: " + df.format(averageFPS));
    	System.out.println("Average UPS: " + df.format(averageUPS));
    	System.out.println("Time Spent: " + timeSpentInGame + " secs");
    }  // end of printStats()

    public void mousePressed(MouseEvent e) {
    	_x = e.getX(); // save x coordinate of the click
    	_y = e.getY(); // save y

    	System.out.println("Mouse Pressed, X:"+_x+" Y: "+_y);
    }

    public void mouseDragged(MouseEvent e) {
    }
    
    public void mouseReleased(MouseEvent e) {
    }
    
    public void mouseMoved   (MouseEvent e) {}
    public void mouseEntered (MouseEvent e) {}
    public void mouseExited  (MouseEvent e) {}
    public void mouseClicked (MouseEvent e) {}
    
    // Key handlers
    public void keyTyped(KeyEvent e) {
  	
    }

    public void keyPressed(KeyEvent e) {
		if (e.getKeyCode() == KeyEvent.VK_Q) {
			stopGame();
		} 
		
    	inputManager.keyPressed(e);
    }

    public void keyReleased(KeyEvent e) {
    	inputManager.keyReleased(e);
    }
}